<!DOCTYPE html>
<html>

<head>
	<link rel="stylesheet" type="text/css" href="style.css" media="screen">
</head>
<meta charset="utf-8" />
<!-- 引入 vue -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.45/vue.global.prod.js"></script>

<!-- 引入样式 -->
<!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.2.25/theme-chalk/index.min.css" rel="stylesheet">

<!-- 引入组件库 -->
<!-- <script src="https://unpkg.com/element-ui/lib/index.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.2.25/index.full.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/@element-plus/icons-vue"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus-icons-vue/2.0.10/global.iife.js"></script>
<!-- qrcode -->
<!-- <script src="https://cdn.jsdelivr.net/npm/qrcodejs2@0.0.2/qrcode.min.js"></script> -->
<script src="wasm_exec.js"></script>


<body>
	<div id="app">
		<el-row>
			<el-col :span="22" :offset="1">
				<el-card>
					<div slot="header">订阅信息</div>
					<el-container :element-loading-text="loadingContent">
						<el-form label-width="120px" size="large">
								<el-form-item label="设置：">
									<el-radio-group v-model="option" :disabled="loading">
										<el-radio :label="0">基础</el-radio>
										<el-radio :label="1">高级</el-radio>
										<el-radio :label="2">导出</el-radio>
										<el-radio :label="3">手动生成</el-radio>
									</el-radio-group>
								</el-form-item>

								<el-form-item label="链接：" v-if="option<2">
									<el-col :span="8">
										<el-input v-model="subscription" style="width: 800px"
										@keyup.enter.native="submit" placeholder="支持 V2Ray/Trojan/SS/SSR/Clash 订阅链接，多个节点链接及文件路径的批量测速"
										:disabled="loading||upload" clearable></el-input>
									</el-col>
									<el-col :span="12" ></el-col>
									<el-col :span="12" style="margin-top: 6px;">
										<el-upload :drag="checkUploadStatus('drag')" :v-if="checkUploadStatus('if')"
										action="" :show-file-list="false" ref="upload" :http-request="handleFileChange"
										:auto-upload="true" :before-upload="beforeUpload">
										<i class="el-icon-upload" v-if="!subscription.length"></i>
										<!--<el-button slot="trigger" type="primary" icon="el-icon-files" :disabled="loading" v-if="!upload">选择配置文件</el-button>-->
										<el-button slot="tip" type="danger" icon="el-icon-close" :disabled="loading"
											v-if="upload" @click="cancelFileUpload">取消文件选择</el-button>
										<div class="el-upload__text" v-if="!subscription.length">
											还可以将配置文件拖到此处，或<em>点击上传</em></div>
										</el-upload>
									</el-col>
									
								</el-form-item>

								<el-form-item label="并发数：" v-if="option<2">
									<el-input v-model="concurrency" style="width: 235px" type="number" min="1" max="50"
										 @keyup.enter.native="submit" :disabled="loading"></el-input>
								</el-form-item>
								<el-form-item label="测试时长(秒)：" v-if="option===1">
									<el-input v-model="timeout" style="width: 235px" type="number" min="5" max="60"
										 @keyup.enter.native="submit" :disabled="loading"></el-input>
								</el-form-item>
								<el-form-item label="去除重复节点：" v-if="option===1">
									<el-checkbox v-model="unique">去重</el-checkbox>
								</el-form-item>
								<el-form-item label="测试项：" v-if="option<2">
									<el-select v-model="speedtestMode" :disabled="loading">
										<el-option v-for="(item, key, index) in init.speedtestModes" :key="index"
											:label="item" :value="key">
										</el-option>
									</el-select>
								</el-form-item>
								<el-form-item label="自定义组名：" v-if="option<2">
									<el-input v-model="groupname" style="width: 235px" 
										@keyup.enter.native="submit" :disabled="loading" clearable></el-input>
									<el-button type="primary" @click="submit" style="margin-left: 20px" v-if="!option"
										:disabled="loading" :loading="loading"><el-icon v-if="!loading" class="el-icon--left"><Check /></el-icon>提 交</el-button>
									<el-button type="danger" @click="terminate" :icon="Close" v-if="!option"
										:disabled="!loading"><el-icon class="el-icon--left"><Close /></el-icon>终 止</el-button>
								</el-form-item>

								<el-form-item label="Ping方式：" v-if="option===1" :disabled="loading">
									<el-select v-model="pingMethod" >
										<el-option v-for="(item, key, index) in init.pingMethods" :key="index" :label="item" :value="key">
										</el-option>
									</el-select>
									<el-button type="primary" @click="submit" style="margin-left: 20px" :icon="Check" :disabled="loading"
										:loading="loading">提 交</el-button>
									<el-button type="danger" @click="terminate" :icon="Close" :disabled="!loading">终 止</el-button>
								</el-form-item>
								
								<!-- export -->
								<el-form-item label="语言：" v-if="option===2" :disabled="loading">
									<el-select v-model="language">
										<el-option key="1" label="EN" value="en"></el-option>
										<el-option key="2" label="中文" value="cn"></el-option>
									</el-select>
								</el-form-item>
								<el-form-item label="字体大小：" v-if="option===2">
									<el-input v-model="fontSize" style="width: 235px" type="number" min="12" max="36"
									 @keyup.enter.native="submit" :disabled="loading"></el-input>
								</el-form-item>
								<el-form-item label="排序方式：" v-if="option===2" :disabled="loading">
									<el-select v-model="sortMethod" >
										<el-option v-for="(item, key, index) in init.sortMethods" :key="index"
											:label="item" :value="key">
										</el-option>
									</el-select>
								</el-form-item>
								<el-form-item label="主题：" v-if="option===2" :disabled="loading">
									<el-select v-model="theme" >
										<el-option v-for="(item, key, index) in init.themes" :key="index"
											:label="item" :value="key">
										</el-option>
									</el-select>
								</el-form-item>
								<el-form-item label="结果数据：" v-if="option===3" :disabled="loading">
									<el-input
										type="textarea"
										:autosize="{ minRows: 5, maxRows: 18}"
										placeholder="input data"
										style="width: 800px"
										v-model="generateResultJSON">
									</el-input>
									<el-button type="primary" @click="generateResult" style="margin-left: 20px" :icon="Check" :disabled="loading"
										:loading="loading">生 成</el-button>
								</el-form-item>
						</el-form>
					</el-container>
				</el-card>

				<br>
				<!--
				<el-card>
					<div slot="header">操作</div>
					<el-container>
						<el-form :inline="true">
							<el-form-item>
								<el-button type="primary" @click="submit">提交</el-button>
							</el-form-item>
							<el-form-item>
								<el-button type="secondary">终止</el-button>
							</el-form-item>
						</el-form>
					</el-container>
				</el-card>
				
				<br>
				-->
				<el-card>
					<el-row style="display: flex;">
						<el-col style="display: flex;align-items: center;" :span="1">
							<div>结果</div>
						</el-col>
						<el-col v-if="result.length" :span="8">
							<el-dropdown trigger="click">
								<el-button size="large" type="primary">
									Actions<el-icon class="el-icon--right"><arrow-down /></el-icon>
								</el-button>
								<template #dropdown>
									<el-dropdown-menu slot="dropdown">
										<el-dropdown-item @click.native="handleCopySub()">复制订阅链接</el-dropdown-item>
										<el-dropdown-item v-if="!loading && result.length" @click.native="handleCopyAvailable()">复制可用节点</el-dropdown-item>
										<el-dropdown-item v-if="multipleSelection.length" @click.native="handleCopy()">复制节点</el-dropdown-item>
										<el-dropdown-item v-if="multipleSelection.length" @click.native="handleSave()">导出节点</el-dropdown-item>
										<!-- <el-dropdown-item @click.native="handleRetest()">重新测试</el-dropdown-item> -->
										<el-dropdown-item v-if="multipleSelection.length" @click.native="handleQRCode()">显示二维码</el-dropdown-item>
										<el-dropdown-item v-if="multipleSelection.length" @click.native="handleExportResult()">导出结果</el-dropdown-item>
									</el-dropdown-menu>
								</template>
							</el-dropdown>
						</el-col>
					</el-row>
					<el-container>
						<el-table :data="result" :cell-style="colorCell" ref="result" 
							:row-key="row => `${row.server}${row.protocol}${row.ping}${row.speed}${row.maxspeed}`"
							@selection-change="handleSelectionChange" @sort-change="handleSortChange">
							<el-table-column type="selection" width="55" :selectable="checkSelectable">
							</el-table-column>
							<!-- <el-table-column label="Group" align="center" prop="group" width="300" sortable>
							</el-table-column> -->
							<el-table-column label="Remark" align="center" prop="remark" min-width="400" sortable>
							</el-table-column>
							<el-table-column label="Server" align="center" prop="server" min-width="160" sortable>
							</el-table-column>
							<el-table-column label="Protocol" align="center" prop="protocol" width="120" sortable
								:filters="[{ text: 'V2Ray', value: 'vmess' }, { text: 'Trojan', value: 'trojan' }, { text: 'ShadowsocksR', value: 'ssr' }, { text: 'Shadowsocks', value: 'ss' }]"
								:filter-method="filterProtocol">
							</el-table-column>
							<!-- <el-table-column label="Loss" align="center" prop="loss" width="100" sortable>
							</el-table-column> -->
							<el-table-column label="Ping" align="center" prop="ping" width="100" sortable="custom"
								:filters="[{ text: 'Available ', value: 'available' }]"
								:filter-method="filterPing">
							</el-table-column>
							<el-table-column label="AvgSpeed" align="center" prop="speed" min-width="150" sortable
								:filters="[{ text: '200KB', value: 204800 }, { text: '500KB', value: 512000 }, { text: '1MB', value: 1048576 }, { text: '2MB', value: 2097152 }, { text: '5MB', value: 5242880 }, { text: '10MB', value: 10485760 },{ text: '15MB', value: 15728640 }, { text: '20MB', value: 20971520 }]"
								:filter-multiple="false"
								:filter-method="filterAvgSpeed"
								:sort-method="speedSort">
							</el-table-column>
							<el-table-column label="MaxSpeed" align="center" prop="maxspeed" min-width="150" sortable
								:filters="[{ text: '200KB', value: 204800 }, { text: '500KB', value: 512000 }, { text: '1MB', value: 1048576 }, { text: '2MB', value: 2097152 }, { text: '5MB', value: 5242880 }, { text: '10MB', value: 10485760 },{ text: '15MB', value: 15728640 }, { text: '20MB', value: 20971520 }]"
								:filter-multiple="false"
								:filter-method="filterMaxSpeed"
								:sort-method="maxSpeedSort">
							</el-table-column>
						</el-table>
					</el-container>
				</el-card>

				<br>

				<div :class="['dashboard', dashboardCollapsed ? 'collapsed' : '']">
					<el-card class="progress">
						<div class="progress-bar" :style="{ 'width': testProgress(result, testCount) + '%' }"></div>
						<div class="progress-inner">
							<div class="progress-item">
								<span>{{ testProgress(result, testCount) }}%</span>
								<div>Progress</div>
							</div>
							<div v-if="!dashboardCollapsed" class="progress-item">
								<span>{{ testOkCount }}/{{ result.length }}</span>
								<div>Ratio</div>
							</div>
							<div v-if="!dashboardCollapsed" class="traffic">
								<span> {{ bytesToSize(totalTraffic) }} </span>
								<div>Traffic</div>
							</div>
							<div v-if="!dashboardCollapsed" class="time">
								<span>{{ formatSeconds(totalTime) }}</span>
								<div>Time</div>
							</div>
						</div>
					</el-card>
					<el-card class="category" v-memo="[result]">
						<ul>
							<li v-if="dashboardCollapsed">
								<span>{{ result.filter(item => item.ping > 0).length }}/{{ result.length }}</span>
								<div>Ratio</div>
							</li>
							<li v-if="!dashboardCollapsed">
								<span>{{ result.filter(item => item.protocol.startsWith("vmess")).length }}</span>
								<div>Vmess</div>
							</li>
							<li v-if="!dashboardCollapsed">
								<span>{{ result.filter(item => item.protocol === "trojan").length }}</span>
								<div>Trojan</div>
							</li>
							<li v-if="!dashboardCollapsed">
								<span>{{ result.filter(item => item.protocol === "ssr").length }}</span>
								<div>SSR</div>
							</li>
							<li v-if="!dashboardCollapsed">
								<span>{{ result.filter(item => item.protocol === "ss").length }}</span>
								<div>SS</div>
							</li>
						</ul>
					</el-card>
					<div class="icon" @click="handleDashboardCollapsed()">
						<el-icon v-if="!dashboardCollapsed"><Right /></el-icon>
						<el-icon v-if="dashboardCollapsed"><Back /></el-icon>
						<!-- <i v-if="!dashboardCollapsed" class="el-icon-right" ></i>
						<i v-if="dashboardCollapsed" class="el-icon-back"></i> -->
					</div>
				</div>

				<br>

				<el-card v-if="picdata.length">
					<div slot="header">导出图片</div>
					<el-container>
						<el-image :src="picdata" fit="true" placeholder="未加载图片" id="result_png"></el-image>
					</el-container>
				</el-card>
			</el-col>
		</el-row>
		<el-dialog title="Share Links with QRcode" center v-model="qrCodeDialogVisible" width="40%"
			@opened="handleQRCodeCreate" :before-close="qrCodeHandleClose">
			<el-scrollbar style="height:360px;">
				<el-row>
					<el-col v-for="(item, index) of multipleSelection" :key="index" :span="12">
						<el-card :body-style="{ padding: '0px', height:'400px'}" shadow="hover"
							style="width: 320px;height: 330px;text-align: center;">
							<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin-top: 15px;">
								<div :id="'qrcode_' + item.id" style="margin-left: 20px;"></div>
								<div class="truncate_remark">{{ item.remark }}</div>
								<div>{{ `${item.ping}ms ${item.speed} ${item.maxspeed}` }}</div>
							</div>
						</el-card>
					</el-col>
				</el-row>
			</el-scrollbar>
		</el-dialog>
	</div>
	<script>

		const go = new Go();
		WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
			go.run(result.instance);
		});

		let themes = {
			"original": {
				colorgroup: [
					[255, 255, 255],
					[128, 255, 0],
					[255, 255, 0],
					[255, 128, 192],
					[255, 0, 0]
				],
				bounds: [0, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024],
			},
			"rainbow": {
				colorgroup: [
						[255, 255, 255],
						[102, 255, 102],
						[255, 255, 102],
						[255, 178, 102],
						[255, 102, 102],
						[226, 140, 255],
						[102, 204, 255],
						[102, 102, 255]
					],
					bounds: [0, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024, 24 * 1024 * 1024, 32 * 1024 * 1024, 40 * 1024 * 1024 ]
			} 
		}

		let interval = 0;

		let resultjson = [];

		const { createApp } = Vue

		const app = createApp({
			data() {
				return {
					title: "Lite Speedtest Web GUI",
					upload: false,
					filecontent: "",
					loading: false,
					subscription: "",
					concurrency: 2,
					timeout: 15,
					unique: true,
					groupname: "",
					loadingContent: "",
					speedtestMode: "all",
					pingMethod: "googleping",
					sortMethod: "rspeed",
					exportMaxSpeed: true,
					method: "SOCKET",
					picdata: "",
					option: 0,
					multipleSelection: [],
					qrCodeDialogVisible: false,
					totalTraffic: 0,
					totalTime: 0,
					language: "en",
					fontSize: 24,
					theme: "rainbow",
					generateResultJSON: "",
					dashboardCollapsed: true,
					testCount: 0,
					testOkCount: 0,

					init: {
						speedtestModes: {
							all: "全部",
							pingonly: "Ping only",
							speedonly: "Speed only",
						},
						pingMethods: {
							googleping: "Google",
							tcping: "TCP",
						},
						sortMethods: {
							rspeed: "speed 倒序",
							speed: "speed 顺序",
							ping: "ping 顺序",
							rping: "ping 倒序",
							none: "默认",
						},
						themes: {
							rainbow: "Rainbow",
							original: "Original",
						}
					},
					result: []
				}
			},
			created() {
				document.title = this.title;
			},
			methods: {
				bytesToSize: function (bytes) {
					const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
					if (!bytes || bytes === 0) return '0 B';
					const i = parseInt(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), 10);
					if (i === 0) return `${bytes} ${sizes[i]})`;
					return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`;
				},
				testProgress: function (result, testCount) {
					return result.length ? Math.floor(testCount/result.length*100) : 0
				},
				formatSeconds: function (seconds) {
					let totalTime = seconds > 0 ? seconds : 0
					const hours = Math.floor(totalTime / 3600);
					totalTime %= 3600;
					const minutes = Math.floor(totalTime / 60);
					const secs = totalTime % 60;
					let result = `${secs}s`
					result = minutes > 0 ? `${minutes}m ${result}` : result
					result = hours > 0 ? `${hours}h ${result}` : result
					return result
				},
				incrTotalTime: function () {
					if (this.totalTime >= 0 && this.loading) {
						this.$nextTick(() => {
							setTimeout(() => {
								this.totalTime++;
								this.incrTotalTime()
							}, 1000);
						})
					}
				},
				cancelFileUpload: function () {
					let self = this;
					this.file = null;
					this.filecontent = '';
					this.subscription = '';
					self.upload = false;
				},
				handleFileChange(e) {
					let self = this;
					this.file = e.file;
					this.errText = '';
					if (!this.file || !window.FileReader) return;
					let reader = new FileReader();
					reader.readAsText(this.file);
					reader.onloadend = function () {
						self.filecontent = this.result;
						self.subscription = self.file.name;
						self.upload = true;
					}
				},
				beforeUpload(file) {
					// const isType = file.type === 'application/json' || file.type === 'application/octet-stream'
					const fsize = file.size / 1024 / 1024 <= 10;
					// if (!isType) {
					// 	this.$message.error('选择的文件格式有误!');
					// }
					if (!fsize) {
						this.$message.error('上传的文件不能超过10MB!');
					}
					return fsize;
				},
				checkUploadStatus(type) {
					if (!this.upload) {
						if (this.subscription.length)
							return false;
						else
							return true;
					}
					else {
						if (type === "if")
							return true;
						else if (type === "drag")
							return false;
					}
				},
				submit: function () {
					this.testCount = 0;
					this.testOkCount = 0;
					if (!this.subscription.length) {
						this.$alert("请先输入链接或选择文件！", "错误", {
							type: "error",
						});
					} else {
						this.$refs.result.clearSelection();
						this.$refs.result.clearFilter();
						this.$refs.result.clearSort();
						this.loading = true;
						this.totalTraffic = 0;
						this.totalTime = 0;
						this.picdata = "";
						this.result = [];
						this.incrTotalTime()
						this.loadingContent = "等待后端响应……";
						this.starttest();
					}
				},
				generateResult: function (params) {
					if (!this.generateResultJSON) {
						return
					}
					const requestOptions = {
						method: "POST",
						headers: { "Content-Type": "application/json" },
						body: this.generateResultJSON
					};
					const url = `${window.location.protocol}//${window.location.host}/generateResult`
					fetch(url, requestOptions)
						.then(resp => resp.text())
						.then(data => {
							this.picdata = data
						})
				},
				terminate: function () {
					this.loading = false;
					this.loadingContent = "等待后端响应……";
					this.result = [];
					this.disconnect();
				},
				handleSelectionChange(val) {
					// console.log(`select: ${JSON.stringify(val)}`)
					this.multipleSelection = val;
				},
				handleSortChange(val) {
					// console.log(`handleSortChange: ${JSON.stringify(val)}`)
					if (val.prop === "ping") { 
						if (val.order === "ascending") {
							this.result.sort((obj1, obj2) => {
								let ping1 = parseFloat(obj1.ping);
								if (ping1 < 1) { ping1 = 99999 }
								let ping2 = parseFloat(obj2.ping);
								if (ping2 < 1) { ping2 = 99999 }
								return ping1 - ping2
							})
						} else if (val.order === "descending") {
							this.result.sort((obj1, obj2) => parseFloat(obj2.ping) - parseFloat(obj1.ping))
						} else {
							this.result.sort((obj1, obj2) => obj1.id - obj2.id)
						}
					 }
				},
				copyToClipboard: async function (data) {
					if (navigator.clipboard) {
							await navigator.clipboard.writeText(data)
						} else {
							let textArea = document.createElement("textarea");
							textArea.value = data;
							// make the textarea out of viewport
							textArea.style.position = "fixed";
							textArea.style.left = "-999999px";
							textArea.style.top = "-999999px";
							document.body.appendChild(textArea);
							textArea.focus();
							textArea.select();
							document.execCommand('copy');
							textArea.remove();
						}
				},
				handleCopySub: async function () {
					// url
					if (this.subscription.trim().startsWith("http") && !this.subscription.trim().endsWith(".yaml") && !this.subscription.trim().endsWith(".yml")) {
						await navigator.clipboard.writeText(this.subscription.trim())
						this.$message.success("Copy Subscription succeed!");
						return
					}
					const host = window.location.host;
					if (host.startsWith("127.0.0.1")) {
						const url = `${window.location.protocol}//${window.location.host}/getSubscriptionLink`
						const groupname = this.groupname.trim() || "Default"
						const requestOptions = {
							method: "POST",
							headers: { "Content-Type": "application/json" },
							body: JSON.stringify({"filePath": this.subscription.trim(), "group": groupname})
						};
						fetch(url, requestOptions)
							.then(resp => resp.text())
							.then(data => {
								navigator.clipboard.writeText(data)
								this.$message.success("Copy Subscription succeed!");
							})
						return
					}
					this.$message.error("Copy Subscription failed!");
				},
				handleCopy: async function () {
					try {
						links = this.multipleSelection.map(elem => elem.link).join("\n")
						await this.copyToClipboard(links)
						this.$message.success("Copy link succeed!");
					} catch (err) {
						this.$message.error("Copy link failed!");
					}
				},
				handleCopyAvailable: async function () {
					try {
						links = this.result.filter(elem => elem.ping > 0).map(elem => elem.link)
						await this.copyToClipboard(links.join("\n"))
						this.$message.success(`Copy ${links.length} link${links.length>1 ? "s" : ""} succeed!`);
					} catch (err) {
						this.$message.error("Copy link failed!");
					}
				},
				qrCodeHandleClose() {
					this.qrCodeDialogVisible = false;
					this.multipleSelection.forEach(item => {
						document.getElementById('qrcode_' + item.id).innerHTML = '';
					});
				},
				handleQRCode() {
					this.qrCodeDialogVisible = true
				},
				handleQRCodeCreate: function () {
					this.$nextTick(() => {
						const items = this.multipleSelection.map(item => {
							return {
								gid: 'qrcode_' + item.id,
								link: item.link,
								size: 260
							}
						})
						wasmQRcode(JSON.stringify(items))
						// this.multipleSelection.forEach(item => {
						// 	const gid = 'qrcode_' + item.id;
						// 	wasmQRcode(gid, item.link, 260, 260)
						// })
					})
				},
				handleRetest: function () {
					// const data = { testid: id, testMode: 3, links: [link], ...this.getJSONOptions() }
					const testids = this.multipleSelection.map(elem => elem.id)
					const links = this.multipleSelection.map(elem => elem.link)
					const data = { testMode: 3, ...this.getJSONOptions(), testids, links }
					this.$refs.result.clearSelection();
					this.$refs.result.clearFilter();
					this.$refs.result.clearSort();
					console.log(`handleRetest: ${JSON.stringify(data)}`)
					this.send(JSON.stringify(data));
				},
				saveData: function (data, name) {
					const blob = new Blob([data], { type: 'text/plain;charset=utf-8;' })
					const link = document.createElement('a')
					if (link == null || link.download == null || link.download == undefined) {
						return
					}
					var event = new Date();
					event.setMinutes(event.getMinutes() - event.getTimezoneOffset());
					let jsonDate = event.toJSON().slice(0, 19);
					jsonDate = jsonDate.replaceAll("-", "")
					jsonDate = jsonDate.replaceAll("T", "")
					jsonDate = jsonDate.replaceAll(":", "")
					let url = URL.createObjectURL(blob)
					link.setAttribute('href', url)
					link.setAttribute('download', `${name}_${jsonDate}`)
					link.style.visibility = 'hidden'
					document.body.appendChild(link)
					link.click()
					document.body.removeChild(link)
					
				},
				handleDashboardCollapsed: function () {
					console.log(this.dashboardCollapsed)
					this.dashboardCollapsed = !this.dashboardCollapsed
				},
				handleSave: function () {
					const links = this.multipleSelection.map(elem => {
							return `# ${elem.remark}\t${elem.ping}\t${elem.speed}\t${elem.maxspeed}\n${elem.link}`
					})
					if (this.subscription.match(/^https?:\/\//g)) {
						links.unshift(`# ${this.subscription}`)
					}
					this.saveData(links.join("\n"), "profile")
				},
				handleExportResult: function (params) {
					const nodes = this.result.map(item => {
						const avg_speed = Math.floor(this.getSpeed(item.speed)) || 0
						const max_speed = Math.floor(this.getSpeed(item.maxspeed)) || 0
						return {
							id: item.id,
							group: item.group,
							remarks: item.remark,
							protocol: item.protocol,
							ping: `${item.ping}`,
							avg_speed,
							max_speed,
							isok: item.ping > 0,
						}
					})
					const data = {
						totalTraffic: this.bytesToSize(this.totalTraffic),
						totalTime: this.formatSeconds(this.totalTime),
						language: this.language,
						fontSize: this.fontSize,
						theme: this.theme,
						sortMethod: this.sortMethod,
						nodes,
					}
					this.saveData(JSON.stringify(data, null, 2), "result")
				},
				colorCell: function ({
					row,
					column,
					rowIndex,
					columnIndex
				}) {
					let style = {color: "black", "font-weight": 600};
					let speed = 0;
					switch (columnIndex) {
						case 5:
							speed = this.getSpeed(row.speed);
							break;
						case 6:
							speed = this.getSpeed(row.maxspeed);
							break;
						default:
							return style;
					}
					if (isNaN(parseFloat(speed))) return style;
					let color = this.getSpeedColor(speed);
					// console.log(`speed: ${speed}, row.speed: ${row.speed}, row.maxspeed: ${row.maxspeed}  color: ${color}`);
					style.background = color
					return style;
				},
				// useNewPalette() {
				// 	colorgroup = [
				// 		[255, 255, 255],
				// 		[102, 255, 102],
				// 		[255, 255, 102],
				// 		[255, 178, 102],
				// 		[255, 102, 102],
				// 		[226, 140, 255],
				// 		[102, 204, 255],
				// 		[102, 102, 255]
				// 	];
				// 	bounds = [
				// 		0,
				// 		64 * 1024,
				// 		512 * 1024,
				// 		4 * 1024 * 1024,
				// 		16 * 1024 * 1024,
				// 		24 * 1024 * 1024,
				// 		32 * 1024 * 1024,
				// 		40 * 1024 * 1024
				// 	];
				// },
				getSpeed(speed) {
					let value = parseFloat(speed.toString().slice(0, -2));
					if (speed.toString().slice(-2) == "MB") {
						value *= 1048576;
					} else if (speed.toString().slice(-2) == "KB") {
						value *= 1024;
					} else value = parseFloat(speed.toString().slice(0, -1));
					return value;
				},
				getColor(lc, rc, level) {
					let colors = [];
					let r, g, b;
					colors.push(parseInt(lc[0] * (1 - level) + rc[0] * level));
					colors.push(parseInt(lc[1] * (1 - level) + rc[1] * level));
					colors.push(parseInt(lc[2] * (1 - level) + rc[2] * level));
					return colors;
				},
				getSpeedColor(speed) {
					const {colorgroup, bounds} = themes[this.theme];
					for (let i = 0; i < bounds.length - 1; i++) {
						if (speed >= bounds[i] && speed <= bounds[i + 1]) {
							let color = this.getColor(
								colorgroup[i],
								colorgroup[i + 1],
								(speed - bounds[i]) / (bounds[i + 1] - bounds[i])
							);
							return "rgb(" + color[0] + "," + color[1] + "," + color[2] + ")";
						}
					}
					return (
						"rgb(" +
						colorgroup[colorgroup.length - 1][0] +
						"," +
						colorgroup[colorgroup.length - 1][1] +
						"," +
						colorgroup[colorgroup.length - 1][2] +
						")"
					);
				},
				connect(url) {
					try {
						ws = new WebSocket(url);
					} catch (ex) {
						this.loading = false;
						//this.$message.error('Cannot connect: ' + ex)
						this.$alert("后端连接错误！请检查后端运行情况！原因：" + ex, "错误");
						return;
					}
				},
				disconnect() {
					if (ws) {
						ws.close();
					}
				},
				send(msg) {
					if (ws) {
						try {
							ws.send(msg);
						} catch (ex) {
							this.$message.error("Cannot send: " + ex);
						}
					} else {
						this.loading = false;
						//this.$message.error('Cannot send: Not connected')
						this.$alert("后端连接错误！请检查后端运行情况！", "错误");
					}
				},
				getJSONOptions() {
					let self = this;
					let groupstr = self.groupname == "" ? "?empty?" : self.groupname;
					// const options = `^${groupstr}^${self.speedtestMode}^${self.pingMethod}^${self.sortMethod}^${self.exportMaxSpeed}^${self.concurrency}^${self.timeout}`
					return {
						group: groupstr,
						speedtestMode: self.speedtestMode,
						pingMethod: self.pingMethod,
						sortMethod: self.sortMethod,
						unique: self.unique,
						concurrency: parseInt(self.concurrency),
						timeout: parseInt(self.timeout),
						language: self.language,
						fontSize: parseInt(self.fontSize),
						theme: self.theme,
					}
				},
				getOptions() {
					let self = this;
					let groupstr = self.groupname == "" ? "?empty?" : self.groupname;
					const options = `^${groupstr}^${self.speedtestMode}^${self.pingMethod}^${self.sortMethod}^${self.exportMaxSpeed}^${self.concurrency}^${self.timeout}`
					return options
				},
				starttest() {
					let self = this;
					let groupstr = self.groupname == "" ? "?empty?" : self.groupname;
					this.result = [];
					this.connect(`ws://${window.location.host}/test`);
					if (ws) {
						ws.addEventListener("open", function (ev) {
							const data = self.getJSONOptions()
							data.testMode = 2
							data.subscription = self.upload ? self.filecontent : self.subscription;
							this.send(JSON.stringify(data));
						});
						ws.addEventListener("message", this.MessageEvent);
					} else {
						this.loading = false;
						this.$alert("后端连接错误！请检查后端运行情况！", "错误");
					}
				},
				loopevent(id, tester) {
					item = this.result[id];
					switch (tester) {
						case "ping":
							item.ping = "测试中...";
							item.loss = "测试中...";
							item.testing = true
							this.result[id]=item;
							break;
						case "speed":
							item.speed = "测试中...";
							item.maxspeed = "测试中...";
							item.testing = true
							this.result[id]=item;
							break;
					}
				},
				MessageEvent(ev) {
					console.log(ev.data);
					let json = JSON.parse(ev.data);
					let id = parseInt(json.id);

					let item = {};
					switch (json.info) {
						case "started":
							this.loadingContent = "后端已启动……";
							break;
						case "fetchingsub":
							this.loadingContent = "正在获取节点，若节点较多将需要一些时间……";
							break;
						case "begintest":
							this.loadingContent = "疯狂测速中……";
							break;
						case "gotserver":
							item = {
								id: id,
								group: this.groupname == "" ? json.group : this.groupname,
								remark: json.remarks,
								server: json.server,
								protocol: json.protocol,
								link: json.link,
								loss: "0.00%",
								ping: "0.00",
								speed: "0.00B",
								maxspeed: "0.00B"
							};
							this.result[id] = item;
							break;
						case "gotservers":
							json.servers.forEach(json => {
								item = {
									id: json.id,
									group: this.groupname == "" ? json.group : this.groupname,
									remark: json.remarks,
									server: json.server,
									protocol: json.protocol,
									link: json.link,
									loss: "0.00%",
									ping: "0.00",
									speed: "0.00B",
									maxspeed: "0.00B"
								};
								this.result[json.id] = item;
							})
							break;							
						case "endone":
							item = this.result[id];
							item.testing = false
							this.result[id] = item;
							break;
						case "startping":
							//inverval=setInterval("app.loopevent("+id+",\"ping\")",300)
							this.loopevent(id, "ping");
							break;
						case "gotping":
							//clearInterval(interval)
							item = this.result[id];
							item.loss = json.loss;
							item.ping = json.ping || 0;
							this.testCount += 1
							if (item.ping > 0) {
								this.testOkCount += 1
							}
							/*
										item = {
											"group": json.group,
											"remark": json.remarks,
											"loss": json.loss,
											"ping": json.ping,
											"speed": "0.00KB"
										}
										*/
							this.result[id] = item;
							break;
						case "startspeed":
							//inverval=setInterval("app.loopevent("+id+",\"speed\")",300)
							this.loopevent(id, "speed");
							break;
						case "gotspeed":
							//clearInterval(interval)
							item = this.result[id];
							item.speed = json.speed;
							item.maxspeed = json.maxspeed;
							this.totalTraffic += json.traffic
							this.result[id] = item;
							break;
						case "picsaving":
							this.$notify.info("保存结果图片中……");
							break;
						case "picsaved":
							this.$notify.success("图片已保存！路径：" + json.path);
							break;
						case "picdata":
							this.picdata = json.data;
							break;
						case "eof":
							this.loading = false;
							console.log(this.result)
							break;
						case "retest":
							item = this.result[id];
							this.$notify.error(
								"节点 " + item.group + " - " + item.remark + " 第一次测试无速度，将重新测试。"
							);
							break;
						case "nospeed":
							item = this.result[id];
							this.$notify.error(
								"节点 " + item.group + " - " + item.remark + " 第二次测试无速度。"
							);
							item.speed = "0.00B";
							item.maxspeed = "0.00B";
							this.result[id] = item;
							break;
						case "error":
							switch (json.reason) {
								case "noconnection":
									item = this.result[id];
									item.ping = "0.00";
									item.loss = "100.00%";
									this.$notify.error(
										"节点 " + item.group + " - " + item.remark + " 无法连接。"
									);
									this.result[id] = item;
									break;
								case "noresolve":
									item = this.result[id];
									item.ping = "0.00";
									item.loss = "100.00%";
									this.$notify.error(
										"节点 " + item.group + " - " + item.remark + " 无法解析到 IP 地址。"
									);
									this.result[id] = item;
									break;
								case "nonodes":
									this.$alert("找不到任何节点。请检查订阅链接。", "错误");
									break;
								case "invalidsub":
									this.$alert("订阅获取异常。请检查订阅链接。", "错误");
									this.terminate()
									break;
								case "norecoglink":
									this.$alert("找不到任何链接。请检查提供的链接格式。", "错误");
									break;
								case "unhandled":
									this.$alert("程序异常退出！", "错误");
									break;
							}
							console.log("error:" + json.reason);
							break;
					}
				},
				floatSort: function (obj1, obj2) {
					return parseFloat(obj1.ping) - parseFloat(obj2.ping);
				},
				speedSort: function (obj1, obj2) {
					const speed1 = isNaN(this.getSpeed(obj1.speed)) ? -1 : this.getSpeed(obj1.speed);
					const speed2 = isNaN(this.getSpeed(obj2.speed)) ? -1 : this.getSpeed(obj2.speed);
					return speed1 - speed2;
				},
				maxSpeedSort: function (obj1, obj2) {
					const speed1 = isNaN(this.getSpeed(obj1.maxspeed)) ? -1 : this.getSpeed(obj1.maxspeed);
					const speed2 = isNaN(this.getSpeed(obj2.maxspeed)) ? -1 : this.getSpeed(obj2.maxspeed);
					return speed1 - speed2;
				},
				filterPing: function (value, row) {
					return value === "available" ? row.ping > 0 : true;
				},
				filterAvgSpeed: function (value, row) {
					const speed = isNaN(this.getSpeed(row.speed)) ? -1 : this.getSpeed(row.speed);
					return speed >= value;
				},
				filterMaxSpeed: function (value, row) {
					const speed = isNaN(this.getSpeed(row.maxspeed)) ? -1 : this.getSpeed(row.maxspeed);
					return speed >= value;
				},
				filterProtocol: function (value, row) {
					if (value === "vmess") {
						return row.protocol.startsWith("vmess")
					}
					if (value === "trojan") {
						return row.protocol.startsWith("trojan")
					}
					return value === row.protocol
				},
				checkSelectable: function (row, index) {
					return !!row.link && row.hasOwnProperty("id") && row.testing !== true
				},
			}
		});
		for ([name, comp] of Object.entries(ElementPlusIconsVue)) {
			app.component(name, comp);
		}
		app.use(ElementPlus);
		app.mount('#app')
	</script>
</body>

</html>